﻿using System;
using System.IO;

namespace SpiderSync
{
    enum SyncAction { copy, delete, rename, resync }

    /// <summary>
    /// Represents a single synchronization operation with action type and path(s)
    /// </summary>
    class SyncItem
    {
        public SyncAction action { get; set; }
        public string path { get; set; }
        public string newPath { get; set; }

        public DateTime date { get; set; }
       
        /// <summary>
        /// Creates an instance from action and path
        /// </summary>
        /// <param name="action">Sync action</param>
        /// <param name="path">Path to sync</param>
        public SyncItem(SyncAction action, string path)
        {
            if (action == SyncAction.rename)
            {
                throw new ArgumentException("Rename syncitems needs new path");
            }

            this.action = action;
            this.path = path;
            this.date = DateTime.Now;
        }
        
        /// <summary>
        /// Creates a resync action
        /// </summary>
        /// <param name="action">SyncAction.resync</param>
        public SyncItem(SyncAction action)
        {
            if (action != SyncAction.resync)
            {
                throw new ArgumentException(action + " syncItems needs at least one path");
            }
            this.action = action;
            this.date = DateTime.Now;
        }

        /// <summary>
        /// Creates a rename SyncItem from old and new path
        /// </summary>
        /// <param name="action">SyncAtion.rename</param>
        /// <param name="path">Old path</param>
        /// <param name="newPath">New path</param>
        private SyncItem(SyncAction action, string path, string newPath)
        {
            if (action != SyncAction.rename)
            {
                throw new ArgumentException("Only actions with rename action needs newPath argument");
            }

            this.action = action;
            this.path = path;
            this.newPath = newPath;
            this.date = DateTime.Now;
        }

        /// <summary>
        /// Creates an instance from a filesystem change or rename event
        /// </summary>        
        /// <param name="evt">EventArrgs object</param>
        /// <returns>A created SyncItem</returns>
        public static SyncItem FromFilesystemEvent(object evt)
        {
            RenamedEventArgs renameEvent = evt as RenamedEventArgs;
            if (renameEvent != null)
            {
                return new SyncItem(SyncAction.rename, renameEvent.OldName, renameEvent.Name);
            }

            FileSystemEventArgs changeEvent = evt as FileSystemEventArgs;
            if (changeEvent == null)
            {          
                throw new ArgumentException("Invalid filsystem change argument received");
            }

            return new SyncItem(GetActionFromFilesystemEvent(changeEvent), changeEvent.Name);
        }
               
        public override bool Equals(object obj)
        {
            SyncItem other = obj as SyncItem;
            if ((object)other == null)
            {
                return false;
            }

            return this.GetHashCode() == obj.GetHashCode();
        }

        public override int GetHashCode()
        {
            int hashCode = (int)this.action;

            if (this.path != null)
            {
                hashCode ^= path.GetHashCode();
            }

            if (this.newPath != null)
            {
                hashCode ^= newPath.GetHashCode();
            }

            return hashCode;
        }

        public static bool operator ==(SyncItem a, SyncItem b)
        {
            if (object.ReferenceEquals(a, b))
            {
                return true;
            }

            if (((object)a == null) || ((object)b == null))
            {
                return false;
            }

            return a.Equals(b);
        }

        public static bool operator !=(SyncItem a, SyncItem b)
        {
            return !(a == b);
        }

        public override string ToString()
        {
            string value = this.action + " " + this.path;
            if (this.action == SyncAction.rename)
            {
                value += " -> " + this.newPath;
            }

            return value;
        }

        /// <summary>
        /// Perform sync action on the specified folders
        /// </summary>
        /// <param name="sourceRoot">Source root folder</param>
        /// <param name="destinationRoot">Destination root folder</param>
        /// <param name="retries">Sync retry count</param>
        public void DoSync(string sourceRoot, string destinationRoot, int retries)
        {
            string sourcePath;
            string destinationPath = this.GetDestinationPath(destinationRoot);

            switch (this.action)
            {
                case SyncAction.copy:
                    sourcePath = Path.Combine(sourceRoot, this.path);
                    FilesystemUtils.Copy(sourcePath, destinationPath);
                    break;

                case SyncAction.delete:
                    FilesystemUtils.Delete(destinationPath);
                    break;

                case SyncAction.rename:
                    sourcePath = Path.Combine(destinationRoot, this.path);
                    if (FilesystemUtils.PathExists(sourcePath))
                    {
                        FilesystemUtils.Rename(sourcePath, destinationPath);
                    }
                    else
                    {
                        sourcePath = Path.Combine(sourceRoot, this.newPath);
                        FilesystemUtils.Copy(sourcePath, destinationPath);
                    }
                    break;

                case SyncAction.resync:
                    FilesystemUtils.SyncDirectories(sourceRoot, destinationRoot, false, retries);
                    break;
            }
        }           

        /// <summary>
        /// Get full destination path from destination root
        /// </summary>
        /// <param name="destinationRoot">Destination root directory</param>
        /// <returns></returns>
        public string GetDestinationPath(string destinationRoot)
        {
            return this.path == null ? destinationRoot : Path.Combine(destinationRoot, this.action == SyncAction.rename ? this.newPath : this.path);
        }

        /// <summary>
        /// Get SynAction from FileSystemEventArgs
        /// </summary>
        /// <param name="evt">FileSystemEventArgs to check/param>
        /// <returns>SyncAction</returns>
        protected static SyncAction GetActionFromFilesystemEvent(FileSystemEventArgs evt)
        {
            switch (evt.ChangeType)
            {
                case WatcherChangeTypes.Changed:
                case WatcherChangeTypes.Created:
                    return SyncAction.copy;

                case WatcherChangeTypes.Deleted:
                    return SyncAction.delete;

                case WatcherChangeTypes.Renamed:
                    return SyncAction.rename;

                default:
                    throw new NotSupportedException("Received not supported filesystem change type");
            }
        }
    }
}
